package org.apache.lucene.ocean.database;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.ocean.CommitResult;
import org.apache.lucene.ocean.Snapshot;
import org.apache.lucene.ocean.TransactionSystem;
import org.apache.lucene.ocean.Index.IndexSnapshot;
import org.apache.lucene.ocean.util.Constants;
import org.apache.lucene.ocean.util.LongSequence;
import org.apache.lucene.ocean.util.NumberUtils;
import org.apache.lucene.ocean.util.Util;
import org.apache.lucene.search.Query;

/**
 * Assigns unique ID for each OceanObject. The OceanObject may be updated in
 * place versioned using optimistic concurrency. To search on numbers or dates
 * and have them return properly in range queries or sorted results, they need
 * to be Field.Index.UN_TOKENIZED.
 * 
 */
// TODO: Write query parser that takes into account encoding numbers to the
// appropriate string value
// TODO: automatically remove constant fields
public class OceanDatabaseImpl implements OceanDatabase {
  private final TransactionSystem transactionSystem;
  private LongSequence objectIdSequence;

  public OceanDatabaseImpl(TransactionSystem transactionSystem) throws IOException {
    this.transactionSystem = transactionSystem;
    Snapshot snapshot = transactionSystem.getSnapshots().getLatestSnapshot();
    if (snapshot != null) {
      List<Long> snapshotObjectIds = new ArrayList<Long>();
      for (IndexSnapshot indexSnapshot : snapshot.getIndexSnapshots()) {
        String idStr = indexSnapshot.getMax(Constants.ID);
        if (idStr != null) {
          Long id = Util.longFromEncoded(idStr);
          snapshotObjectIds.add(id);
        }
      }
      Long maxObjectId = Util.max(snapshotObjectIds);
      int serverNumber = transactionSystem.getConfig().serverNumber;
      if (maxObjectId != null) {
        // if there already is a maxObjectId, start from the next iteration of
        // it
        Long objectSequenceId = Util.getNextServerSequence(maxObjectId, serverNumber);
        objectIdSequence = new LongSequence(objectSequenceId, 100);
      } else {
        // no previous object ids start from the server number
        objectIdSequence = new LongSequence(serverNumber, 100);
      }
    }
  }

  public Object run(Task task) throws Exception {
    return task.run(transactionSystem);
  }

  public Snapshot getSnapshot() {
    return transactionSystem.getSnapshots().getLatestSnapshot();
  }

  /**
   * Update action checks the current snapshot to see if the version is correct,
   * if it is not an exception is thrown
   */
  public ActionResult perform(Action action) throws Exception {
    List<Document> documents = new ArrayList<Document>();
    List<Term> deleteByTerms = new ArrayList<Term>();
    List<Query> deleteByQueries = new ArrayList<Query>();
    Analyzer analyzer = null;
    OceanObject newObject = null;
    if (action instanceof Insert) {
      Insert insert = (Insert) action;
      System.out.println("insert");
      // remove constant fields
      insert.object.remove(Constants.ID);
      insert.object.remove(Constants.DATE_CREATED);
      insert.object.remove(Constants.DATE_MODIFIED);
      insert.object.remove(Constants.VERSION);
      insert.object.remove(Constants.ID_VERSION);
      // add an object id
      long id = objectIdSequence.getAndIncrement();
      insert.object.add(new OceanField(Constants.ID, id, Store.YES, Index.UN_TOKENIZED));
      // add the dates
      Date date = new Date();
      insert.object.add(new OceanField(Constants.DATE_CREATED, date, Store.YES, Index.UN_TOKENIZED));
      insert.object.add(new OceanField(Constants.DATE_MODIFIED, date, Store.YES, Index.UN_TOKENIZED));
      // add the version which is 1
      insert.object.add(new OceanField(Constants.VERSION, new Long(1), Store.YES, Index.UN_TOKENIZED));
      insert.object.add(new OceanField(Constants.ID_VERSION, id+"_1", Store.YES, Index.UN_TOKENIZED));
      Document document = insert.object.toDocument();
      documents.add(document);
      analyzer = insert.object.getAnalyzer();
      newObject = insert.object;
    } else if (action instanceof Update) {
      Update update = (Update) action;
      Snapshot snapshot = getSnapshot();
      transactionSystem.getCommitLock().lock();
      try {
        try {
          // lookup to make sure the version is correct
          // TODO: need to get the commit lock here to insure a new version does
          // not get in
          MultiReader multiReader = new MultiReader(snapshot.getIndexReaders());
          Long id = (Long)update.object.getValue(Constants.ID);
          if (id == null) 
            throw new Exception("id null");
          Long version = (Long)update.object.getValue(Constants.VERSION);
          if (version == null) 
            throw new Exception("version null");
          int maxDoc = multiReader.maxDoc();
          System.out.println("maxDoc: "+maxDoc);
          /**
          TermEnum termEnum = multiReader.terms(new Term(Constants.ID, ""));
          do {
            Term term = termEnum.term();
            String text = term.text();
            System.out.println("text: "+text);
            String str = NumberUtils.sortableStr2long(text);
            System.out.println("term: "+str);
          } while (termEnum.next());
          **/
          int doc = Util.getTermDoc(new Term(Constants.ID, NumberUtils.long2sortableStr(id)), multiReader);
          if (doc > -1) {
            long[] versions = Util.getFieldCacheLong(Constants.VERSION, multiReader);
            if (version.longValue() == versions[doc]) { // versions are the same
              Term deleteByTerm = new Term(Constants.ID, NumberUtils.long2sortableStr(id));
              deleteByTerms.add(deleteByTerm);
            } else {
              throw new Exception("document " + id + " version: " + version + " index version: " + versions[doc]);
            }
          } else {
            throw new Exception("document " + id + " not found");
          }
          update.object.remove(Constants.ID_VERSION);
          update.object.remove(Constants.DATE_MODIFIED);
          update.object.add(new OceanField(Constants.DATE_MODIFIED, new Date(), Store.YES, Index.UN_TOKENIZED));
          update.object.remove(Constants.VERSION);
          // set the new version which is 1 greater than the previous
          update.object.add(new OceanField(Constants.VERSION, version + 1, Store.YES, Index.UN_TOKENIZED));
          update.object.add(new OceanField(Constants.ID_VERSION, id+"_"+update.object.getVersion(), Store.YES, Index.UN_TOKENIZED));
          Document document = update.object.toDocument();
          documents.add(document);
          analyzer = update.object.getAnalyzer();
          newObject = update.object;
        } finally {
          snapshot.decRef();
        }
      } finally {
        transactionSystem.getCommitLock().unlock();
      }
    } else if (action instanceof Delete) {
      Delete delete = (Delete) action;
      Term deleteByTerm = new Term(Constants.ID, NumberUtils.long2sortableStr(delete.id));
      deleteByTerms.add(deleteByTerm);
    } else if (action instanceof DeleteQuery) {
      DeleteQuery deleteQuery = (DeleteQuery) action;
      deleteByQueries.add(deleteQuery.query);
    }
    CommitResult commitResult = transactionSystem.commitTransaction(documents, analyzer, deleteByTerms, deleteByQueries);
    return new ActionResult(action.actionId, newObject);
  }
}
